Skip to content

feat(admin): show org slug in detail URLs instead of UUID#1763

Open
Shreyag02 wants to merge 3 commits into
mainfrom
feat/admin-org-slug-urls
Open

feat(admin): show org slug in detail URLs instead of UUID#1763
Shreyag02 wants to merge 3 commits into
mainfrom
feat/admin-org-slug-urls

Conversation

@Shreyag02

Copy link
Copy Markdown
Contributor

Summary

Admin organization detail pages now show the org's readable slug (its name) in the URL instead of the raw UUID
(e.g. /organizations/acme-corp instead of /organizations/6f3a…).
All API calls still run on the org id — the slug is display-only — so the change is purely about the URL, not the data layer.

Changes

  • Org detail URLs use the slug for a readable, shareable address.
  • The org id travels with in-app navigation (list, admins, create, audit) via router state, and is used for every org RPC.
  • Cold loads (refresh, bookmark, old UUID links) fall back to resolving the URL id/slug, with a loader while it resolves.
  • Breadcrumb navigates client-side (no full reload); tab links keep the slug from the current URL.
  • Old /organizations/<uuid> links keep working.

Technical Details

  • Why id, not slug, for RPCs:
    A disabled org's slug stops resolving server-side (its name is freed on disable), so the id is the only reliable key. getOrganization is therefore called by id, and so are the id-scoped RPCs (KYC, members, roles, billing).
  • How the id reaches the page:
    Navigators pass { state: { orgId } } alongside the slug URL. The detail page holds that id across tab switches (which
    clear router state).
  • Cold-load fallback:
    With no router state, the page resolves the URL segment via getOrganization — an id or an enabled org's slug both resolve; only a disabled org's slug can't, and that redirects to the list. This keeps deep-linking working without adding a per-load request waterfall (id-scoped RPCs still fire in parallel).
  • Admins/audit views resolve the org by id (useOrganizationLookup) to display its title and build the slug link.

Test Plan

  • Manual testing completed
    • List → org detail shows a slug URL; members/projects/tokens/etc. tabs load and stay on the slug.
    • Breadcrumb (org title + "Organizations") navigates without a full reload.
    • Audit log → organization link and Create org → redirect both land on the correct detail page.
    • Disabled org opened from the list works (id via state).
    • Refresh / paste a slug URL resolves (loader → page); old UUID link still works; an unresolvable slug redirects to the list.
  • Build and type checking passes (admin app tsc clean; SDK rebuilt)

SQL Safety (if your PR touches *_repository.go or goqu.*)

N/A

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 16, 2026 11:33am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Improvements
    • Organization navigation now uses readable names in URLs while retaining reliable organization identifiers.
    • Organization details remain available when navigating between tabs or loading pages directly.
    • Organization names are displayed in admin and audit-log views, with identifier fallbacks when needed.
    • Audit-log navigation now preserves organization context.
    • Creating an organization now opens its details using the new navigation behavior.

Walkthrough

Organization navigation now separates URL slugs from organization IDs carried in router state. Organization details resolve IDs from navigation state or URL lookup, while audit-log links propagate organization state through client-side navigation.

Changes

Organization-aware navigation

Layer / File(s) Summary
Organization navigation contracts and entry points
web/sdk/admin/hooks/useOrganizationLookup.ts, web/sdk/admin/views/admins/*, web/sdk/admin/views/organizations/list/*, web/apps/admin/src/pages/admins/AdminsPage.tsx, web/apps/admin/src/pages/organizations/list/index.tsx
Admin and organization list callbacks now pass (slug, orgId), use slugs in paths, preserve IDs in router state, and resolve organization names for admin cells.
Organization detail ID resolution
web/apps/admin/src/pages/organizations/details/index.tsx, web/sdk/admin/views/organizations/details/layout/navbar.tsx
Organization details resolve IDs from router state or URL lookup, display loading or redirect states, and preserve slug-based tab and breadcrumb navigation.
Audit-log organization navigation
web/sdk/admin/views/audit-logs/*, web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
Audit-log navigation callbacks and side-panel links now accept optional organization state and forward it through client-side navigation.
Estimated code review effort: 3 (Moderate) ~25 minutes

Possibly related PRs

Suggested reviewers: rohilsurana, rsbh

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
web/sdk/admin/views/organizations/details/layout/navbar.tsx (2)

241-243: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback to organizationId if the parsed segment is empty.

While currentPath usually includes the organization slug, adding an explicit fallback protects against edge cases where the extracted segment evaluates to an empty string (e.g., if the path exactly matches orgPrefix without a trailing segment).

♻️ Proposed refactor
   const orgSegment = currentPath.startsWith(orgPrefix)
-    ? currentPath.slice(orgPrefix.length).split("/")[0]
+    ? currentPath.slice(orgPrefix.length).split("/")[0] || organizationId
     : organizationId;

319-337: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract navigation paths to reduce duplication.

You can extract the string literals for the paths into variables to avoid duplicating them between the href and onNavigate callbacks.

♻️ Proposed refactor
@@ -311,27 +311,28 @@
     search.onChange(value);
   }
 
+  const listPath = `/${adminPaths.organizations}`;
+  const detailsPath = `${listPath}/${organization?.name || organization?.id}`;
+
   return (
     <nav className={styles.navbar}>
       <Flex gap={4} align="center">
         <Breadcrumb size="small">
           <Breadcrumb.Item
-            href={`/${adminPaths.organizations}`}
+            href={listPath}
             onClick={(e) => {
               e.preventDefault();
-              onNavigate(`/${adminPaths.organizations}`);
+              onNavigate(listPath);
             }}
             leadingIcon={<OrganizationIcon />}
           >
             {t.organization({ plural: true, case: "capital" })}
           </Breadcrumb.Item>
           <Breadcrumb.Separator />
           {/* Navigate client-side so we don't full-reload and lose the held org id. */}
           <Breadcrumb.Item
-            href={`/${adminPaths.organizations}/${organization?.name || organization?.id}`}
+            href={detailsPath}
             onClick={(e) => {
               e.preventDefault();
-              onNavigate(
-                `/${adminPaths.organizations}/${organization?.name || organization?.id}`,
-              );
+              onNavigate(detailsPath);
             }}
             leadingIcon={

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 95db9072-ec2e-4afc-8448-2a4967c46c75

📥 Commits

Reviewing files that changed from the base of the PR and between 7dcfcee and 37fdcad.

📒 Files selected for processing (13)
  • web/apps/admin/src/pages/admins/AdminsPage.tsx
  • web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/apps/admin/src/pages/organizations/list/index.tsx
  • web/sdk/admin/hooks/useOrganizationLookup.ts
  • web/sdk/admin/views/admins/columns.tsx
  • web/sdk/admin/views/admins/index.tsx
  • web/sdk/admin/views/audit-logs/index.tsx
  • web/sdk/admin/views/audit-logs/sidepanel-details.tsx
  • web/sdk/admin/views/audit-logs/sidepanel-list-link.tsx
  • web/sdk/admin/views/organizations/details/layout/navbar.tsx
  • web/sdk/admin/views/organizations/list/create.tsx
  • web/sdk/admin/views/organizations/list/index.tsx

Comment on lines +32 to +50
const [stateOrgId, setStateOrgId] = useState<string | undefined>(
(location.state as { orgId?: string } | null)?.orgId,
);

// New id on org switch; keep the current one on tab switches.
useEffect(() => {
const next = (location.state as { orgId?: string } | null)?.orgId;
if (next && next !== stateOrgId) setStateOrgId(next);
}, [location.state, stateOrgId]);

// Cold-load fallback: resolve from the URL only when state has no id.
const needsResolve = !stateOrgId && !!urlParam;
const { data: resolvedId, error: resolveError } = useQuery(
FrontierServiceQueries.getOrganization,
{ id: urlParam || "" },
{ enabled: needsResolve, select: (data) => data?.organization?.id },
);

const orgId = stateOrgId || resolvedId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Key the cached organization ID to the current URL parameter.

On an organization switch, the previous stateOrgId is used for the first render because the effect runs after commit. If the new location has no orgId state, the old ID is never cleared and URL resolution remains disabled, potentially loading organization A under organization B’s URL.

Cache both the URL parameter and ID, and only reuse the ID when the cached parameter still matches.

Proposed fix
-  const [stateOrgId, setStateOrgId] = useState<string | undefined>(
-    (location.state as { orgId?: string } | null)?.orgId,
-  );
+  const navigationOrgId =
+    (location.state as { orgId?: string } | null)?.orgId;
+  const [cachedNavigation, setCachedNavigation] = useState(() => ({
+    urlParam,
+    orgId: navigationOrgId,
+  }));

-  useEffect(() => {
-    const next = (location.state as { orgId?: string } | null)?.orgId;
-    if (next && next !== stateOrgId) setStateOrgId(next);
-  }, [location.state, stateOrgId]);
+  useEffect(() => {
+    if (navigationOrgId) {
+      setCachedNavigation({ urlParam, orgId: navigationOrgId });
+    }
+  }, [navigationOrgId, urlParam]);
+
+  const stateOrgId =
+    navigationOrgId ??
+    (cachedNavigation.urlParam === urlParam
+      ? cachedNavigation.orgId
+      : undefined);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [stateOrgId, setStateOrgId] = useState<string | undefined>(
(location.state as { orgId?: string } | null)?.orgId,
);
// New id on org switch; keep the current one on tab switches.
useEffect(() => {
const next = (location.state as { orgId?: string } | null)?.orgId;
if (next && next !== stateOrgId) setStateOrgId(next);
}, [location.state, stateOrgId]);
// Cold-load fallback: resolve from the URL only when state has no id.
const needsResolve = !stateOrgId && !!urlParam;
const { data: resolvedId, error: resolveError } = useQuery(
FrontierServiceQueries.getOrganization,
{ id: urlParam || "" },
{ enabled: needsResolve, select: (data) => data?.organization?.id },
);
const orgId = stateOrgId || resolvedId;
const navigationOrgId =
(location.state as { orgId?: string } | null)?.orgId;
const [cachedNavigation, setCachedNavigation] = useState(() => ({
urlParam,
orgId: navigationOrgId,
}));
useEffect(() => {
if (navigationOrgId) {
setCachedNavigation({ urlParam, orgId: navigationOrgId });
}
}, [navigationOrgId, urlParam]);
const stateOrgId =
navigationOrgId ??
(cachedNavigation.urlParam === urlParam
? cachedNavigation.orgId
: undefined);
// Cold-load fallback: resolve from the URL only when state has no id.
const needsResolve = !stateOrgId && !!urlParam;
const { data: resolvedId, error: resolveError } = useQuery(
FrontierServiceQueries.getOrganization,
{ id: urlParam || "" },
{ enabled: needsResolve, select: (data) => data?.organization?.id },
);
const orgId = stateOrgId || resolvedId;

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29494862156

Coverage remained the same at 45.987%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 38426
Covered Lines: 17671
Line Coverage: 45.99%
Coverage Strength: 13.29 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants